package org.openarchitectureware.recipe2.workflow;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Expand;
import org.apache.tools.ant.taskdefs.Jar;
import org.apache.tools.ant.taskdefs.Javac;
import org.apache.tools.ant.taskdefs.Mkdir;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;
import org.eclipse.emf.mwe.core.WorkflowContext;
import org.eclipse.emf.mwe.core.WorkflowInterruptedException;
import org.eclipse.emf.mwe.core.issues.Issues;
import org.eclipse.emf.mwe.core.lib.AbstractWorkflowComponent2;
import org.eclipse.emf.mwe.core.monitor.ProgressMonitor;
import org.eclipse.internal.xtend.expression.ast.SyntaxElement;
import org.eclipse.xpand2.Generator;
import org.eclipse.xpand2.output.Outlet;
import org.eclipse.xtend.expression.EvaluationException;
import org.eclipse.xtend.expression.ExceptionHandler;
import org.eclipse.xtend.expression.ExecutionContext;
import org.eclipse.xtend.typesystem.MetaModel;
import org.openarchitectureware.recipe2.dsl.detector.Detector;
import org.openarchitectureware.recipe2.dsl.detector.RecipeDetectorModel;
import org.openarchitectureware.recipe2.dsl.recipeBuilder.PluginTemplate;
import org.openarchitectureware.recipe2.dsl.recipeBuilder.PredefTemplate;
import org.openarchitectureware.recipe2.dsl.recipeBuilder.PredefTemplatePackage;
import org.openarchitectureware.recipe2.dsl.recipeBuilder.RecipeBuilderFactory;
import org.openarchitectureware.recipe2.dsl.recipeBuilder.RecipeBuilderModel;

public class DefaultRecipePluginBuilder extends AbstractWorkflowComponent2 implements RecipeBuilder {

	private static final String COMPONENT_NAME = "Recipe Plugin Builder";

	private RecipeDetectorModel detectors;
	protected final Log log = LogFactory.getLog(getClass());
	private final List<MetaModel> metaModels = new ArrayList<MetaModel>();
	private final List<String> classpathUris = new ArrayList<String>();
	protected ExceptionHandler exceptionHandler = null;
	private Object classpathURIContext;
	private String recipeGenSrc = "gen-recipe";
	private String srcDir = "src";
	private String binDir = "bin";
	private String distDir = "dist";
	private List<String> pluginJarUris = new ArrayList<String>();
	private String recipeJarName = "recipe.jar";

	private String fileEncoding;
	private String detectorModelSlot = RecipeGenerator.DEFAULT_DETECTORS_SLOT;

	private String detectorSlot = "recipeDetector";

	private String outletName = "RECIPE";
	private Project antProject;

	private RecipeBuilderModel builderModel;

	public void addClasspathUri(final String uri) {
		assert uri != null;
		classpathUris.add(uri);
	}
	public void addMetaModel(final MetaModel metaModel) {
		assert metaModel != null;
		metaModels.add(metaModel);
	}

	public void addPluginJarUri(String pluginJarUri) {
		assert pluginJarUri != null;
		this.pluginJarUris.add(pluginJarUri);
	}

	@Override
	protected void checkConfigurationInternal(Issues issues) {
	}

	protected void compile(File srcPath, File binPath) {
		Javac javac = new Javac();
		javac.setProject(getAntProject());

		Path srcAntPath = new Path(getAntProject());
		srcAntPath.setPath(srcPath.getPath());
		javac.setSrcdir(srcAntPath);

		javac.setDestdir(binPath);
		for (String classpathUri : classpathUris) {
			Path classpath = javac.createClasspath(); 
			classpath.setProject(getAntProject());
			File classpathFile = new File(java.net.URI.create(classpathUri));
			String classpathEntry = classpathFile.getPath();
			classpath.createPathElement().setPath(classpathEntry);
		}
		javac.execute();
	}

	private FileSet fileSet(File binPath) {
		FileSet bin = new FileSet();
		bin.setDir(binPath);
		return bin;
	}

	protected void generate(RecipeBuilderModel builderModel, WorkflowContext ctx, ProgressMonitor monitor, Issues issues) {
		// generate per-detector templates
		for (Detector detector : detectors.getBugDetectors()) {
			String predef = detector.getPredef();
			PredefTemplate predefTemplate = getPredefTemplate(builderModel, predef);
			if (predefTemplate == null) {
				log.trace("No template registered for predef " + predef);
			} else {
				generate(predefTemplate.getTemplate(), detectorSlot, detector, ctx, monitor, issues);
			}
		}

		// generate per-plugin templates
		for (PluginTemplate template : builderModel.getPluginTemplates()) {
			generate(template.getTemplate(), detectorModelSlot, detectors, ctx, monitor, issues);
		}
	}

	protected void generate(String template, String slot, Object element, WorkflowContext ctx, ProgressMonitor monitor, Issues issues) {
		Generator generator = new Generator();

		Outlet outlet = new Outlet(getSrcPath());
		outlet.setName(outletName);
		outlet.setOverwrite(true);
		generator.addOutlet(outlet);

		generator.setFileEncoding(fileEncoding);

		String invoke = template + " FOR " + slot;
		ctx.set(slot, element);
		generator.setExpand(invoke);
		for (MetaModel metaModel : metaModels) {
			generator.addMetaModel(metaModel);
		}

		generator.invoke(ctx, monitor, issues);
	}

	protected void generateRecipe(WorkflowContext ctx, ProgressMonitor monitor, Issues issues) {
		initDetectorsModel(ctx);

		// generate code to recipeGenSrc/srcDir
		log.info("Generating to " + getSrcPath());
		generate(builderModel, ctx, monitor, issues);

		// compile code to recipeGenSrc/binDir
		File srcPath = new File(getSrcPath());
		File binPath = new File(getBinPath());
		log.info("Compiling to " + getBinPath());
		mkdir(binPath);
		compile(srcPath, binPath);

		// jar it all to recipeGenSrc/distDir
		File distPath = new File(getDistPath());
		File jarPath = new File(distPath, recipeJarName);
		mkdir(distPath);
		jarPlugin(srcPath, binPath, pluginJarUris, jarPath);

	}
	

	private Project getAntProject() {
		if (antProject == null) {
			antProject = new Project();
			antProject.setBasedir(".");
		}
		return antProject;
	}

	private String getBinPath() {
		return new File(recipeGenSrc, binDir).getPath();
	}

	public Object getClasspathURIContext() {
		return classpathURIContext;
	}

	@Override
	public String getComponentName() {
		return COMPONENT_NAME;
	}

	private String getDistPath() {
		return new File(recipeGenSrc, distDir).getPath();
	}

	/**
	 * @see org.eclipse.emf.mwe.core.lib.AbstractWorkflowComponent#getLogMessage()
	 */
	@Override
	public String getLogMessage() {
		return "Generating Recipe Plugin to '" + recipeGenSrc + "'";
	}

	private PredefTemplate getPredefTemplate(RecipeBuilderModel builderModel, String predef) {
		for (PredefTemplate template : builderModel.getPredefTemplates()) {
			if (template.getName().equals(predef)) {
				return template;
			}
		}
		PredefTemplatePackage templatePattern = getPredefTemplatePackage(builderModel, predef);
		if (templatePattern == null) {
			return null;
		}
		PredefTemplate template = RecipeBuilderFactory.eINSTANCE.createPredefTemplate();
		template.setName(predef);
		template.setTemplate(templatePattern.getTemplatePackage() + "::" + predef);
		return template;
	}

	private PredefTemplatePackage getPredefTemplatePackage(RecipeBuilderModel builderModel, String predef) {
		PredefTemplatePackage templatePattern = null;
		for (PredefTemplatePackage tp : builderModel.getPredefTemplatePatterns()) {
			if (tp.getName().equals(predef)) {
				templatePattern = tp;
			}
		}
		return templatePattern;
	}

	public String getRecipeJarName() {
		return recipeJarName;
	}

	private String getSrcPath() {
		return new File(recipeGenSrc, srcDir).getPath();
	}

	private void initDetectorsModel(WorkflowContext ctx) {
		Object slotContent = ctx.get(detectorModelSlot);
		if (detectors == null) {
			if (slotContent instanceof RecipeDetectorModel) {
				detectors = (RecipeDetectorModel) slotContent;
			}
		} else {
			if (slotContent != detectors) {
				ctx.set(detectorModelSlot, detectors);
			}
		}
	}

	@Override
	protected void invokeInternal(WorkflowContext ctx, ProgressMonitor monitor, final Issues issues) {
		try {
			if (exceptionHandler == null) {
				exceptionHandler = new ExceptionHandler() {
					public void handleRuntimeException(RuntimeException ex, SyntaxElement element, ExecutionContext ctx, Map<String, Object> additionalContextInfo) {
						issues.addError(DefaultRecipePluginBuilder.this, ex.getMessage(), element);
						throw ex;
					}
				};
			}
			generateRecipe(ctx, monitor, issues);
		} catch (EvaluationException e) {
			log.error("Error in Component" + (getId() == null ? " " : " " + getId()) + " of type " + getClass().getName());
			throw new WorkflowInterruptedException(e.getMessage());
		}
	}

	protected void jarPlugin(File srcPath, File binPath, List<String> pluginJarPaths, File jarPath) {
		Jar jar = new Jar();
		jar.setProject(getAntProject());

		if (pluginJarPaths != null) {
			for (String pluginJarPathString : pluginJarPaths) {
				log.info("Including plugin resources: " + pluginJarPathString);
				java.net.URI pluginUri = java.net.URI.create(pluginJarPathString);
				File pluginJarPath = null;
				try {
					pluginJarPath = new File(pluginUri);
				} catch (Exception e) {
					throw new WorkflowInterruptedException("Error creating plugin jar file reference " + pluginUri, e);
				}
				
				if (!pluginJarPath.exists()) {
					throw new WorkflowInterruptedException("pluginJarUri does not exist as a file or directory: " + pluginJarPath);
				}
				if (pluginJarPath.isFile()) {
					Expand unjar = new Expand();
					unjar.setProject(getAntProject());
					unjar.setDest(binPath);
					unjar.setSrc(pluginJarPath);
					unjar.setOverwrite(true);
					unjar.execute();
				} else {
					jar.addFileset(fileSet(pluginJarPath));
				}
			}	
		}

		jar.addFileset(fileSet(srcPath));
		jar.addFileset(fileSet(binPath));
		jar.setDestFile(jarPath);

		log.info("Archiving to " + jarPath);
		jar.execute();
	}



	protected void mkdir(File dir) {
		Mkdir mkdir = new Mkdir();
		mkdir.setDir(dir);
		mkdir.setProject(getAntProject());
		mkdir.execute();
	}

	public void setBinDir(String binDir) {
		this.binDir = binDir;
	}
	
	public void setBuilderModel(RecipeBuilderModel builderModel) {
		this.builderModel = builderModel;
	}

	public void setClasspathURIContext(Object classpathURIContext) {
		this.classpathURIContext = classpathURIContext;
	}

	public void setDetectorModelSlot(String detectorModelSlot) {
		this.detectorModelSlot = detectorModelSlot;
	}

	public void setDetectors(RecipeDetectorModel detectorModel) {
		detectors = detectorModel;
	}

	public void setDetectorSlot(String detectorSlot) {
		this.detectorSlot = detectorSlot;
	}

	public void setDistDir(String distDir) {
		this.distDir = distDir;
	}

	public void setExceptionHandler(final ExceptionHandler exceptionHandler) {
		this.exceptionHandler = exceptionHandler;
	}

	public void setFileEncoding(String fileEncoding) {
		this.fileEncoding = fileEncoding;
	}

	public void setRecipeGenSrc(String src) {
		recipeGenSrc = src;
	}

	public void setRecipeJarName(String recipeJarName) {
		this.recipeJarName = recipeJarName;
	}

	public void setSrcDir(String srcDir) {
		this.srcDir = srcDir;
	}
}
